contents

객체지향 프로그래밍(OOP) vs 함수형 프로그래밍(FP): 상세 설명과 코드 예제

**OOP(객체지향 프로그래밍)**과 **FP(함수형 프로그래밍)**의 차이를 이해하는 것은 현대 소프트웨어 개발에서 매우 중요합니다. 아래에서는 두 패러다임의 개념, 핵심 원칙, 차이점, 그리고 실전 코드 예제를 포함한 심층 비교를 제공합니다.

1. 객체지향 프로그래밍 (OOP)

OOP란?

OOP는 데이터를 **객체(Objects)**의 형태로 모델링하는 프로그래밍 패러다임입니다. 객체는 **데이터(필드)**와 **행동(메서드)**를 함께 묶은 단위로, 현실 세계의 개체와 관계를 표현하는 데 초점이 있습니다.

OOP의 4대 핵심 원칙 (4대 특성)

  1. 캡슐화(Encapsulation): 데이터와 메서드를 묶고 외부로부터 내부를 숨기기
  2. 추상화(Abstraction): 필수적인 데이터만 외부에 노출
  3. 상속(Inheritance): 클래스 간의 속성과 행동 공유
  4. 다형성(Polymorphism): 동일한 인터페이스로 다양한 구현 수행 가능

어떤 경우에 사용?

Java 기반 예제

abstract class Animal {
    String name;
    public Animal(String name) { this.name = name; }
    public abstract void speak();
}

class Dog extends Animal {
    public Dog(String name) { super(name); }
    public void speak() { System.out.println(name + " says: 멍멍!"); }
}

class Cat extends Animal {
    public Cat(String name) { super(name); }
    public void speak() { System.out.println(name + " says: 야옹!"); }
}

public class Zoo {
    public static void main(String[] args) {
        Animal a1 = new Dog("바비");
        Animal a2 = new Cat("키티");
        a1.speak(); // 다형성: Dog.speak() 호출
        a2.speak(); // 다형성: Cat.speak() 호출
    }
}

2. 함수형 프로그래밍 (Functional Programming, FP)

FP란?

FP는 수학적 함수의 평가를 기반으로 하여 실행되는 패러다임으로, **불변성(immutability)**과 **순수 함수(pure function)**를 강조합니다.

FP의 핵심 개념

어떤 경우에 사용?

Kotlin 예제

fun square(x: Int): Int = x * x

fun applyToList(lst: List<Int>, f: (Int) -> Int): List<Int> = lst.map(f)

val nums = listOf(1, 2, 3, 4)
val squared = applyToList(nums, ::square)
println(squared) // [1, 4, 9, 16]

val evensSum = nums.filter { it % 2 == 0 }.sum()
println(evensSum) // 6 (2+4)

Java Streams 예제

List<Integer> nums = Arrays.asList(1, 2, 3, 4);

List<Integer> squares = nums.stream()
    .map(x -> x * x)
    .toList(); // [1, 4, 9, 16]

int sumEven = nums.stream()
    .filter(x -> x % 2 == 0)
    .mapToInt(x -> x)
    .sum(); // 6

3. OOP vs FP 차이점 정리

항목 객체지향 프로그래밍(OOP) 함수형 프로그래밍(FP)
주요 구성 요소 클래스, 객체 함수
데이터 처리 방식 변경 가능한 상태 (mutable) 변경 불가 상태 (immutable)
코드 구조 데이터 + 함수 결합 순수 함수, 상태 없음
상태 방식 객체 내부에 상태 저장 상태 없이 함수 체인으로 흐름 구성
사이드 이펙트 허용, 종종 기대됨 피해야 함 (예: 전역변수, 로그 출력 등 없음)
추상화 방식 상속, 객체 합성 함수 합성, 고차 함수
테스트 난이도 내부 상태와 사이드 이펙트로 인해 테스트 어려움 순수 함수 덕에 테스트 용이
주 사용 언어 Java, C++, C#, Python 등 Haskell, Kotlin, JavaScript(함수형 스타일), Scala 등

4. 실전 예시 비교: 짝수만 제곱한 리스트 만들기

OOP 스타일 (Java)

public class ListProcessor {
    public List<Integer> getEvenSquares(List<Integer> numbers) {
        List<Integer> result = new ArrayList<>();
        for (Integer n : numbers) {
            if (n % 2 == 0) {
                result.add(n * n);
            }
        }
        return result;
    }
}

FP 스타일 (Kotlin)

val numbers = listOf(1,2,3,4,5,6)
val evenSquares = numbers.filter { it % 2 == 0 }.map { it * it }
println(evenSquares) // [4, 16, 36]

5. 함께 사용할 수 있을까?

6. 요약 테이블

항목 OOP 예시 FP 예시
캡슐화(Encapsulation) user.getName() 불필요
불변성(immutability) 상태 변경 허용 변경하지 않고 새 객체 반환
상속(Inheritance) class Dog extends Animal 상속 대신 함수 합성
함수 전달/반환 인터페이스/전략 패턴을 통해 전달 함수를 직접 인자나 결과로 사용 가능
코드 스타일 클래스 기반, 상태 집중 함수 기반, 선언형 스타일

✅ 결론

Java Streams 기반 함수형 프로그래밍(FP) vs Spring 기반 객체지향 프로그래밍(OOP) 고급 예제 및 실무 활용

아래는 동일한 비즈니스 시나리오를 Java Streams를 활용한 함수형 스타일(FP) 과 **Spring 서비스를 활용한 객체지향 스타일(OOP)**로 각각 구현한 고급 예제를 통해, 두 패러다임이 어떻게 현대 엔터프라이즈 개발에서 함께 사용될 수 있는지를 보여줍니다.

1. 함수형 프로그래밍 예시 (Java Streams 기반)

시나리오: 🛒 전자상거래 시스템에서의 주문 요약 처리

목표:

Java Streams 기반 구현

public class Order {
    enum Status { PAID, PENDING, CANCELLED }
    private final String customerId;
    private final double total;
    private final Status status;

    // 생성자 및 getter 생략
}

List<Order> orders = ...; // 예: DB에서 불러온 주문 리스트

Map<String, Double> customerTotals = orders.stream()
    .filter(o -> o.getStatus() == Order.Status.PAID)
    .collect(Collectors.groupingBy(
        Order::getCustomerId,
        Collectors.summingDouble(Order::getTotal)
    ));

List<Map.Entry<String, Double>> summary = customerTotals.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .collect(Collectors.toList());

summary.forEach(entry ->
    System.out.println("고객: " + entry.getKey() + " | 총액: " + entry.getValue())
);

☑️ 특징

2. Spring OOP 스타일 예제: 주문 생성 및 결제 처리 프로세스

시나리오: 🛠 주문 생성 → 재고 확인 → 결제 처리 → 알림 발송

전통적 Spring 기반 객체지향 설계

@Service
public class OrderService {
    private final InventoryService inventory;
    private final PaymentService payment;
    private final NotificationService notification;

    @Autowired
    public OrderService(InventoryService inventory, PaymentService payment, NotificationService notification) {
        this.inventory = inventory;
        this.payment = payment;
        this.notification = notification;
    }

    public void processOrder(Order order) {
        if (!inventory.reserve(order)) throw new OutOfStockException();
        if (!payment.charge(order)) throw new PaymentFailedException();
        notification.orderConfirmed(order);
    }
}

각 컴포넌트는 단일 책임 원칙(SRP)을 준수하며 독립적으로 교체/테스트 가능합니다.

3. FP vs OOP 비교: "최근 한 달 내 결제 완료 고객 중 상위 3명에게 쿠폰 발송"

A. Java Streams 로직 (FP 방식)

List<Order> orders = ...;
LocalDate aMonthAgo = LocalDate.now().minusMonths(1);

Map<String, Double> totals = orders.stream()
    .filter(o -> o.getStatus() == Order.Status.PAID && o.getDate().isAfter(aMonthAgo))
    .collect(Collectors.groupingBy(Order::getCustomerId,
        Collectors.summingDouble(Order::getTotal)));

List<String> top3 = totals.entrySet().stream()
    .sorted(Map.Entry.<String, Double>comparingByValue().reversed())
    .limit(3)
    .map(Map.Entry::getKey)
    .collect(Collectors.toList());

B. Spring 서비스로 통합 (OOP 방식)

@Service
public class PromotionService {
    private final OrderRepository orderRepo;
    private final EmailService emailService;

    @Autowired
    public PromotionService(OrderRepository orderRepo, EmailService emailService) {
        this.orderRepo = orderRepo;
        this.emailService = emailService;
    }

    public void sendTopCustomerCoupons() {
        List<Order> orders = orderRepo.findPaidInLastMonth();
        Map<String, Double> totals = aggregateTotals(orders);
        List<String> top3 = getTopN(totals, 3);
        for (String customerId : top3) {
            emailService.sendCoupon(customerId, "VIP 쿠폰 코드입니다!");
        }
    }

    private Map<String, Double> aggregateTotals(List<Order> orders) { /* FP 코드 재사용 */ }
    private List<String> getTopN(Map<String, Double> map, int n) { /* FP 코드 재사용 */ }
}

→ 핵심 데이터 처리 로직은 FP로, 시스템 조율 및 흐름 관리는 OOP로 처리

4. 추가 도메인별 예제

FP: 로그 분석, 데이터 파이프라인, ETL

List<String> logs = ...;
long errorCount = logs.stream()
    .filter(line -> line.contains("ERROR"))
    .count();

Set<String> users = logs.stream()
    .map(this::extractUserId)
    .collect(Collectors.toSet());

OOP: 할인 정책 전략 구조, 마이크로서비스 조합

public interface DiscountStrategy {
    boolean isApplicable(Order order);
    BigDecimal calculateDiscount(Order order);
}

@Component
public class HolidayDiscount implements DiscountStrategy { ... }

@Service
public class DiscountService {
    @Autowired List<DiscountStrategy> strategies;

    public BigDecimal getBestDiscount(Order order) {
        return strategies.stream()
            .filter(s -> s.isApplicable(order))
            .map(s -> s.calculateDiscount(order))
            .max(BigDecimal::compareTo).orElse(BigDecimal.ZERO);
    }
}

→ 각 전략은 OOP로 구성되고, 내부 연산은 FP 스트림으로 처리

5. 요약 비교표

항목 함수형(FP, Java Streams) 객체지향(Spring 기반 OOP)
대표 장점 선언적, 병렬/스트림 연산에 이상적 의존성 주입과 캡슐화, 구조적 표현
좋은 용도 필터링, 그룹핑, 집계, 데이터 파이프라인 도메인 서비스, 트랜잭션, 워크플로우
적용 위치 메서드 내부 로직, DAO/Repository 등 서비스 계층, 애플리케이션 조합/조율
테스트 용이성 매우 높음 (순수 함수 기반) 모킹/DI로 우수한 테스트 구조

✅ 결론

references